04. MainCoroutineRule and Injecting Dispatchers

L5 P4 A04 MainCoroutineRule And Injecting Dispatchers V3

In this step you'll create the MainCoroutineRule JUnit rule and use it in TasksViewModelTest and DefaultTasksRepositoryTest.

Step 1: Add MainCoroutineRule

  1. Create a new class called MainCoroutineRule.kt in the root folder of the test source set.
  2. Copy over the following code toMainCoroutineRule.kt:

MainCoroutineRule.kt

@ExperimentalCoroutinesApi
class MainCoroutineRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()):
   TestWatcher(),
   TestCoroutineScope by TestCoroutineScope(dispatcher) {
   override fun starting(description: Description?) {
       super.starting(description)
       Dispatchers.setMain(dispatcher)
   }

   override fun finished(description: Description?) {
       super.finished(description)
       cleanupTestCoroutines()
       Dispatchers.resetMain()
   }
}

Step 2: Use MainCoroutineRule in TasksViewModelTest

Now use your new rule.

  1. Open TasksViewModelTest.
  2. Replace TestDispatcher code you just wrote (the @Before and @After code to swap and cleanup the dispatcher) with code to use your new MainCoroutineRule:

TasksViewModelTest.kt

// REPLACE
@ExperimentalCoroutinesApi
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()

@ExperimentalCoroutinesApi
@Before
fun setupDispatcher() {
    Dispatchers.setMain(testDispatcher)
}

@ExperimentalCoroutinesApi
@After
fun tearDownDispatcher() {
    Dispatchers.resetMain()
    testDispatcher.cleanupTestCoroutines()
}


// WITH
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
  1. Run completeTask_dataAndSnackbarUpdated, it should work exactly the same!

Step 3: Use MainCoroutineRule for repository testing.

You can also use this rule in DefaultTasksRepositoryTest.

  1. Open up test > data > source > DefaultTasksRepositoryTest.kt
  2. Add the MainCoroutineRule inside of the DefaultTasksRepositoryTest class:

DefaultTasksRepositoryTest.kt

// Set the main coroutines dispatcher for unit testing.
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()

Remember that MainCoroutineRule swaps the Dispatcher.Main for a TestCoroutineDispatcher.

  1. Use Dispatcher.Main, instead of Dispatcher.Unconfined when defining your repository under test:

DefaultTasksRepositoryTest.kt

@Before
fun createRepository() {
    tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
    tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
    // Get a reference to the class under test.
    tasksRepository = DefaultTasksRepository(
    // HERE Swap Dispatcher.Unconfined
        tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Main
    )
}

Generally, only create one TestCoroutineDispatcher to run a test. Whenever you call runBlockingTest, it will create a new TestCoroutineDispatcher if you don't specify one. MainCoroutineRule includes a TestCoroutineDispatcher. So, to ensure that you don't accidentally create multiple instances of TestCoroutineDispatcher, use the mainCoroutineRule.runBlockingTest instead of just runBlockingTest.

  1. Replace runBlockingTest with mainCoroutineRule.runBlockingTest:

DefaultTasksRepositoryTest.kt

// REPLACE
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {

// WITH
fun getTasks_requestsAllTasksFromRemoteDataSource() = mainCoroutineRule.runBlockingTest {
  1. Run your DefaultTasksRepositoryTest class and confirm everything works as before!

Awesome job! Now you're using TestCoroutineDispatcher in your code, which is a preferable dispatcher for testing. Next you'll see how to use an additional feature of the TestCoroutineDispatcher, controlling coroutine execution timing.


MainCoroutineRule and many of the other concepts covered here are explained in detail in the talk Testing Coroutines on Android.